<?php
require_once $_SERVER['DOCUMENT_ROOT'] . '/../include/apps/janus/templates/MarkupTemplateReplacement.php';

/**
 * A class to load templates and perform placeholder replacements for output.
 * @author Mike Taylor (m.j.taylor@leedsmet.ac.uk)
 *
 */
class MarkupTemplate
{
	/**
	 * Prefix string to demarcate the start of replacement codes
	 * @var string
	 */
	private static $codePrefix_ = '{{';
	
	/**
	 * Suffix string to demarcate the end of replacement codes
	 * @var string
	 */
	private static $codeSuffix_ = '}}';
	
	/**
	 * Regular expression for replacement code validation
	 * @var string
	 */
	private static $codeValidationRegex_ = '/[a-z0-9_-]+/i';
	
	/**
	 * Regular expression for finding replacement codes within content
	 * @var string
	 */
	private static $codeExtractionRegex_ = '/\{\{[a-z0-9_-]+}}/i';
	
	/**
	 * Stores the original markup loaded from a template
	 * @var string
	 */
	private $markup_ = '';
	
	/**
	 * Stores the processed markup with all current replacements applied
	 * @var string
	 */
	private $outputMarkup_ = '';
	
	/**
	 * Stores the current list of MarkupTemplateReplacement for replacement processing
	 * @var array
	 */
	private $replacements_ = array();
	
	/**
	 * Flag denoting whether the output markup is updated with the current replacements
	 * @var bool
	 */
	private $isOutputUpToDate_ = false;

	/**
	 * Loads markup from the contents of a file.
	 * 
	 * Optionally automatically detecting replacement codes and adding them to the replacements list
	 * 
	 * @param string $filename Name of the file to read
	 * @param bool $shouldCreateReplacements Flag to indicate whether to automatically detect and add replacement codes from the markup
	 * @param bool $useIncludePath Flag to indicate whether to use the include path when loading the file
	 */
	public function loadTemplateFile($filename, $shouldCreateReplacements, $useIncludePath = false)
	{
		$fileAsString = file_get_contents($filename, $useIncludePath);
		
		if ($fileAsString !== false)
		{
			$this->markup_ = $fileAsString;
			
			if ($shouldCreateReplacements == true)
			{
				$this->createReplacementsFromMarkup();
			}
			
			$this->isOutputUpToDate_ = false;
			return true;
		}
		return false;
	}
	
	/**
	 * Loads markup from a string.
	 * 
	 * Optionally automatically detecting replacement codes and adding them to the replacements list
	 * 
	 * @param string $template String to load markup from
	 * @param bool $shouldCreateReplacements Flag to indicate whether to automatically detect and add replacement codes from the markup
	 */
	public function loadTemplateString($template, $shouldCreateReplacements)
	{
		if (is_string($template))
		{
			$this->markup_ = $template;
			
			if ($createReplacements == true)
			{
				$this->createReplacementsFromMarkup();
			}
			
			$this->isOutputUpToDate_ = false;
			return true;
		}
		return false;
	}
	
	/**
	 * Adds an item to the replacements list
	 * 
	 * @param string $code The text code to be replaced (without prefix and suffix characters)
	 * @param string $replacement The text that will replace the code
	 * @param bool $allowHtmlReplacement Flag to indicate whether to allow HTML content in the replacement
	 */
	public function addReplacement($code, $replacement, $allowHtmlReplacement = false)
	{
		$isValidCode = preg_match(MarkupTemplate::$codeValidationRegex_, $code) == 1 ? true : false;
		
		if ($isValidCode)
		{
			// Don't want to add a replacement for a code that already exists 
			$currentKey = getReplacementKey($code);
			$replacementExists = !is_null($currentKey);
			
			if (!$replacementExists)
			{
				$this->replacements_[] = new MarkupTemplateReplacement($code, $replacement, $allowHtmlReplacement);
				
				// Must update the up-to-date flag when replacements have changed
				$this->isOutputUpToDate_ = false;
				return true;
			}
		}

		return false;
	}
	
	/**
	 * Removes the items from the replacements list that match the specified code
	 * 
	 * @param string $code The text code for the replacements to be removed
	 */
	public function removeReplacement($code)
	{
		$isValidCode = preg_match(MarkupTemplate::$codeValidationRegex_, $code) == 1 ? true : false;
		
		if ($isValidCode)
		{
			$codeMatchCount = $this->countCodeOccurrences($code);
			$codeExists     = $codeMatchCount > 0;

			// If there are multiple entries for a single code (somehow), remove them all
			while ($codeMatchCount > 0)
			{
				$removeKey = $this->getReplacementKey($code);
				unset($this->replacements_[$removeKey]);
				
				$codeMatchCount--;
			}

			// Must update the up-to-date flag when replacements have changed
			if ($codeExists)
			{
				$this->isOutputUpToDate_ = false;
				return true;
			}
		}

		return false;
	}
	
	/**
	 * Updates an item in the replacements list that matches the specified code
	 * 
	 * @param string $code The text code for the replacement to update
	 * @param string $replacement The updated text that will replace the code
	 * @param bool $allowHtmlReplacement Flag to indicate whether to allow HTML content in the replacement
	 */
	public function updateReplacement($code, $replacement = null, $allowHtmlReplacement = null)
	{
		$isValidCode = preg_match(MarkupTemplate::$codeValidationRegex_, $code) == 1 ? true : false;
		$updateKey   = $this->getReplacementKey($code); 
		
		// Don't try to update when the code is invalid, or the code does not already exist 
		if ($isValidCode && !is_null($updateKey))
		{
			if (!is_null($replacement))
			{
				$this->replacements_[$updateKey]->setReplacement($replacement);

				// Must update the up-to-date flag when replacements have changed
				$this->isOutputUpToDate_ = false;
			}
			
			if (!is_null($allowHtmlReplacement))
			{
				if ($allowHtmlReplacement === true)
				{
					$this->replacements_[$updateKey]->enableHtmlReplacement();
				}
				else
				{
					$this->replacements_[$updateKey]->disableHtmlReplacement();
				}
				
				// Must update the up-to-date flag when replacements have changed
				$this->isOutputUpToDate_ = false;
			}

			return true;
		}

		return false;
	}
	
	/**
	 * Returns the markup with all current code replacements applied
	 * 
	 * This function will only process the original markup and replacements list
	 * if the output is NOT up to date. Otherwise the current output is returned.
	 */
	public function getOutputMarkup()
	{
		if ($this->isOutputUpToDate_ !== true)
		{
			$this->updateOutputMarkup();
		}
		
		return $this->outputMarkup_;
	}
	
	/**
	 * Updates the output markup by applying all current replacements to the
	 * original markup
	 */
	private function updateOutputMarkup()
	{
		$this->outputMarkup_ = $this->markup_;
		
		// There has to be at least one replacement set up in order to continue
		if (is_array($this->replacements_) && count($this->replacements_) > 0)
		{
			foreach ($this->replacements_ as $replacement)
			{
				$code                 = $replacement->getCode();
				$replacementContent   = $replacement->getReplacement();
				$allowHtmlReplacement = $replacement->getAllowHtmlReplacement();
				
				// The actual replacement code requires the appropriate prefix and suffix
				$replacementCode = MarkupTemplate::$codePrefix_ . $code . MarkupTemplate::$codeSuffix_;

				if ($allowHtmlReplacement)
				{
					$this->outputMarkup_ = str_replace($replacementCode, $replacementContent, $this->outputMarkup_);
				}
				else
				{
					$this->outputMarkup_ = str_replace($replacementCode, htmlspecialchars($replacementContent), $this->outputMarkup_);
				}
			}
		}
		
		// Must update the up-to-date flag
		$this->isOutputUpToDate_ = true;
	}
	
	/**
	 * Counts the number of instances of replacements for a particular text code
	 * 
	 * @param string $code The text code to search for instances of
	 */
	private function countCodeOccurrences($code)
	{
		$occurrenceCount = 0;
		
		foreach ($this->replacements_ as $key => $replacement)
		{
			if ($replacement->getCode() == $code)
			{
				$occurrenceCount++;
			}
		}
		
		return $occurrenceCount;
	}
	
	/**
	 * Returns the array key for the first instance of a replacement that
	 * matches the specified text code
	 * 
	 * @param string $code The text code to search for an instance of
	 */
	private function getReplacementKey($code)
	{
		if (count($this->replacements_) > 0)
		{
			foreach ($this->replacements_ as $key => $replacement)
			{
				if ($replacement->getCode() == $code)
				{
					return $key;
				}
			}
		}
		
		return null;
	}
	
	/**
	 * Detects demarcated text codes in the markup and fills the replacements list with what's found
	 */
	private function createReplacementsFromMarkup()
	{
		$matches = null;
		
		preg_match_all(MarkupTemplate::$codeExtractionRegex_, $this->markup_, $matches);
		
		if (count($matches) > 0)
		{
			$this->replacements_ = array();
			
			foreach ($matches[0] as $match)
			{
				$match = str_replace(MarkupTemplate::$codePrefix_, '', $match);
				$match = str_replace(MarkupTemplate::$codeSuffix_, '', $match);

				$newReplacement = new MarkupTemplateReplacement($match, '', false);
				$this->replacements_[] = $newReplacement;
			}
		}
	}
}

